// Copyright 2013 Volker Dobler. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package cookiejar implements an in-memory RFC 6265-compliant http.CookieJar.
package cookiejar

import (
	"code.google.com/p/go.net/idna"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"sort"
	"strings"
	"sync"
	"time"
)

// Jar implements the http.CookieJar interface from the net/http package.
type Jar struct {
	psList PublicSuffixList

	// mu locks the remaining fields.
	mu sync.Mutex

	// entries is a set of entries, keyed by their eTLD+1 and subkeyed by
	// their name/domain/path.
	entries map[string]map[string]entry
}

// Options are the options for creating a new Jar.
// TODO: document zero value
type Options struct {
	// PublicSuffixList is the public suffix list that determines whether an
	// HTTP server can set a cookie for a domain. A nil value is valid and
	// may be useful for testing but it is not secure: it means that the
	// HTTP server for foo.com can set a cookie for bar.com.
	//
	// TODO: correct above doc.
	PublicSuffixList PublicSuffixList

	// Other options may be added in Go 1.2 and beyond.
}

// New returns a new cookie jar.
func New(o *Options) (*Jar, error) {
	var psl PublicSuffixList = tldOnly{}
	if o != nil && o.PublicSuffixList != nil {
		psl = o.PublicSuffixList
	}

	jar := &Jar{
		psList:  psl,
		entries: make(map[string]map[string]entry),
	}

	return jar, nil
}

// SetCookies implements the SetCookies method of the http.CookieJar interface.
//
// It does nothing if the URL's scheme is not HTTP or HTTPS.
func (jar *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
	if u.Scheme != "http" && u.Scheme != "https" {
		return
	}
	host, err := canonicalHost(u.Host)
	if err != nil {
		return
	}
	key, _ := jarKey(host, jar.psList)
	if key == "" {
		return
	}
	if len(cookies) == 0 {
		return
	}
	defaultpath := defaultPath(u.Path)

	jar.mu.Lock()
	defer jar.mu.Unlock()

	submap := jar.entries[key]
	now := time.Now()

	modified := false
	for _, cookie := range cookies {
		cookie, remove, err := newEntry(cookie, now, defaultpath, host, jar.psList)
		if err != nil {
			continue
		}
		id := cookie.id()
		if remove {
			if submap != nil {
				if _, ok := submap[id]; ok {
					delete(submap, id)
					modified = true
				}
			}
			continue
		}
		if submap == nil {
			submap = make(map[string]entry)
		}

		if old, ok := submap[id]; ok {
			cookie.Creation = old.Creation
		} else {
			cookie.Creation = now
		}
		cookie.LastAccess = now
		submap[id] = cookie
		modified = true
		// make Creation and LastAccess strictly monotonic to force
		// deterministic behaviour during sorting
		now = now.Add(time.Nanosecond)
	}

	if modified {
		jar.entries[key] = submap
	}
}

// jarKey returns the key to use for a jar.
func jarKey(host string, psl PublicSuffixList) (string, error) {
	key, err := effectiveTLDPlus1(host, psl)
	if err != nil {
		return "", err // BUG return Host here
	}
	return key, nil
}

// effectiveTLDPlus1 returns the effective top level domain plus one more
// label. For example, the eTLD+1 for "foo.bar.golang.org" is "golang.org".
func effectiveTLDPlus1(host string, psl PublicSuffixList) (string, error) {
	suffix := psl.PublicSuffix(host)
	if suffix == host {
		return host, nil
	}
	i := len(host) - len(suffix)
	if i <= 0 || host[i-1] != '.' {
		return "", fmt.Errorf("cookiejar: Bad public suffix %q for host %q", suffix, host)
	}
	return host[1+strings.LastIndex(host[:i], "."):], nil
}

// entry is the internal representation of a cookie.
// The fields are those of RFC 6265.
type entry struct {
	Name       string
	Value      string
	Domain     string
	Path       string
	Secure     bool
	HttpOnly   bool
	Persistent bool
	HostOnly   bool
	Expires    time.Time
	Creation   time.Time
	LastAccess time.Time
}

// Id returns the id of e.
func (e *entry) id() string {
	return fmt.Sprintf("%s;%s;%s", e.Name, e.Domain, e.Path) // TODO: order?
}

// shouldSend determines whether the cookie c qualifies to be included in a
// request to host/path. It is the callers responsibility to check if the
// cookie is expired.
func (e *entry) shouldSend(https bool, host, path string) bool {
	return e.domainMatch(host) && e.pathMatch(path) && e.secureEnough(https)
}

// every cookie is sent via https.  If the protocol is just http, then the
// cookie must not be marked as secure.
func (e *entry) secureEnough(requestIsSecure bool) bool {
	return requestIsSecure || !e.Secure
}

// domainMatch implements "domain-match" of RFC 6265 section 5.1.3:
//   A string domain-matches a given domain string if at least one of the
//   following conditions hold:
//     o  The domain string and the string are identical.  (Note that both
//        the domain string and the string will have been canonicalized to
//        lower case at this point.)
//     o  All of the following conditions hold:
//        *  The domain string is a suffix of the string.
//        *  The last character of the string that is not included in the
//           domain string is a %x2E (".") character.
//        *  The string is a host name (i.e., not an IP address).
func (e *entry) domainMatch(host string) bool {
	if e.Domain == host {
		return true
	}
	return !e.HostOnly && strings.HasSuffix(host, "."+e.Domain)
}

// pathMatch implements "path-match" according to RFC 6265 section 5.1.4:
//   A request-path path-matches a given cookie-path if at least one of
//   the following conditions holds:
//     o  The cookie-path and the request-path are identical.
//     o  The cookie-path is a prefix of the request-path, and the last
//        character of the cookie-path is %x2F ("/").
//     o  The cookie-path is a prefix of the request-path, and the first
//        character of the request-path that is not included in the cookie-
//        path is a %x2F ("/") character.
func (e *entry) pathMatch(requestPath string) bool {
	if requestPath == e.Path { // the simple case
		return true
	}

	if strings.HasPrefix(requestPath, e.Path) {
		if e.Path[len(e.Path)-1] == '/' {
			return true // "/any/path" matches "/" and "/any/"
		} else if requestPath[len(e.Path)] == '/' {
			return true // "/any" matches "/any/some"
		}
	}

	return false
}

// newEntry creates an entry from a http.Cookie c. now is the current time and
// compared to c.Expires to determine deletion of c. defPath and host are the
// default-path and the canonical host name of the URL c was received from.
// psl is used to check wheter c can be a domain cookie.
//
// newEntry will return with remove=true if c indicates to delete this cookie.
// In this case only the identifying fields (Name, Domain, Path) of e are det up.
// Malformed Domain fields in c are result in an error return.
func newEntry(c *http.Cookie, now time.Time, defPath, host string, psl PublicSuffixList) (e entry, remove bool, err error) {
	// the commmon stuff
	e.Name = c.Name

	// proper path
	if c.Path == "" || c.Path[0] != '/' {
		e.Path = defPath
	} else {
		e.Path = c.Path
	}

	// domain and domain vs. host cookie
	e.Domain, e.HostOnly, err = domainAndType(host, c.Domain, psl)
	if err != nil {
		return e, false, err
	}

	// expiery: MaxAge takes precedence over Expires
	if c.MaxAge < 0 {
		return e, true, nil
	} else if c.MaxAge > 0 {
		e.Expires = now.Add(time.Duration(c.MaxAge) * time.Second)
		e.Persistent = true
	} else {
		if c.Expires.IsZero() {
			e.Expires = endOfTime
			e.Persistent = false
		} else {
			if c.Expires.Before(now) {
				return e, true, nil
			}
			e.Expires = c.Expires
			e.Persistent = true
		}
	}

	e.Value = c.Value
	e.Secure = c.Secure
	e.HttpOnly = c.HttpOnly

	return e, false, nil
}

var (
	errNoHostname      = errors.New("cookiejar: No hostname (IP only) available")
	errMalformedDomain = errors.New("cookiejar: Domain attribute of cookie is malformed")
	errIllegalDomain   = errors.New("cookiejar: Illegal cookie domain attribute")
)

// endOfTime is the time when session (non-persistent) cookies expire.
// This instant is representable in most date/time formates and should
// be far enogh in the future.
var endOfTime = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)

// domainAndType determines the cookie's domain and hostOnly attribute.
func domainAndType(host, domain string, psl PublicSuffixList) (string, bool, error) {
	if domain == "" {
		// A RFC6265 conforming Host Cookie: no domain given
		return host, true, nil
	}

	// no hostname, but just an IP address
	if isIP(host) {
		// According to RFC 6265 domain-matching includes not beeing
		// an IP address.
		// TODO: This might be relaxed as in common browsers
		return "", false, errNoHostname
	}

	// From here on: If the cookie is valid, it is a domain cookie (with
	// the one exception of a public suffix).
	// See RFC 6265 section 5.2.3.
	if domain[0] == '.' {
		domain = domain[1:] // strip leading dot
	}

	if len(domain) == 0 || domain[0] == '.' {
		// Received either "Domain=." or "Domain=..some.thing":
		// both are illegal.
		return "", false, errMalformedDomain
	}
	domain = strings.ToLower(domain)

	if domain[len(domain)-1] == '.' {
		// Received stuff like "Domain=www.example.com.".
		// Browsers do handle such stuff (actually differently) but
		// RFC 6265 seems to be clear here (e.g. section 4.1.2.3) in
		// requiering a reject.  4.1.2.3 is not normative, but
		// "Domain Matching" (5.1.3) and "Canonicalized Host Names"
		// (5.1.2) are.
		return "", false, errMalformedDomain
	}

	// See RFC 6265 section 5.3 #5
	if ps := psl.PublicSuffix(domain); ps != "" && !strings.HasSuffix(domain, "."+ps) {
		// the "domain is a public suffix" case
		if host == domain {
			return host, true, nil // the exception: a host cookie
		}
		return "", false, errIllegalDomain
	}

	// Domain must domain-match host: www.mycompany.com cannot
	// set cookies for .ourcompetitors.com.
	if host != domain && !strings.HasSuffix(host, "."+domain) {
		return "", false, errIllegalDomain
	}

	return domain, false, nil
}

// SetCookies handles the receipt of the cookies in a reply for the given URL.
func (jar *Jar) Cookies(u *url.URL) []*http.Cookie {
	if u.Scheme != "http" && u.Scheme != "https" {
		return nil
	}

	// set up host, path and secure
	host, err := canonicalHost(u.Host)
	if err != nil {
		return nil
	}

	https := isSecure(u)
	path := u.Path
	if path == "" {
		path = "/"
	}

	key, err := jarKey(host, jar.psList)
	if key == "" {
		return nil
	}

	jar.mu.Lock()
	defer jar.mu.Unlock()

	submap := jar.entries[key]
	if submap == nil {
		return nil
	}

	now := time.Now()

	modified := false
	cookies := make([]entry, 0)
	for id, cookie := range submap {
		if cookie.Persistent && !cookie.Expires.After(now) {
			delete(submap, id)
			modified = true
			continue
		}
		if !cookie.shouldSend(https, host, path) {
			continue
		}
		cookie.LastAccess = now
		submap[id] = cookie
		cookies = append(cookies, cookie)
		modified = true
	}
	if modified {
		if len(submap) == 0 {
			delete(jar.entries, key)
		} else {
			jar.entries[key] = submap
		}
	}

	sort.Sort(byPathLength(cookies))

	httpCookies := make([]*http.Cookie, len(cookies))
	for i, cookie := range cookies {
		httpCookies[i] = &http.Cookie{Name: cookie.Name, Value: cookie.Value}
	}

	return httpCookies
}

// defaultPath returns the "directory" part of an URL's path according to
// RFC 6265 section 5.1.4.
func defaultPath(path string) string {
	if len(path) == 0 || path[0] != '/' {
		return "/" // empty or malformed
	}

	i := strings.LastIndex(path, "/") // path starts with "/", so i != -1
	if i == 0 {
		return "/" // the "/abc" case
	}
	return path[:i] // the "/abc/xyz" and "/abc/xyz/" case
}

// hostPort returns true if host contains a port number. host may be a host name,
// an IPv4 or an IPv6 address.
func hasPort(host string) bool {
	colons := strings.Count(host, ":")
	if colons == 0 {
		return false
	}
	if colons == 1 {
		return true
	}
	if host[0] == '[' && strings.Index(host, "]:") != -1 {
		return true
	}
	return false
}

// canonicalHost strips port from host if present and returns the canonicalized
// form of the resulting hostname as defined by RFC 6265 section 5.1.2.
func canonicalHost(host string) (string, error) {
	var err error
	host = strings.ToLower(host)
	if hasPort(host) {
		host, _, err = net.SplitHostPort(host)
		if err != nil {
			return "", err
		}
	}

	if strings.HasSuffix(host, ".") {
		// strip trailing dot from fully qualified domain names
		host = host[:len(host)-1]
	}

	host, err = idna.ToASCII(host)
	if err != nil {
		return "", err
	}
	return host, nil
}

// isSecure checks for https scheme in u.
func isSecure(u *url.URL) bool {
	return strings.ToLower(u.Scheme) == "https"
}

// isIP check if host is formaly an IP address.
func isIP(host string) bool {
	ip := net.ParseIP(host)
	return ip != nil
}

// byPathLength is a list of entries which can be sorted according to
// RFC 6265 section 5.4 point 2:
//  - cookie with longer path goes firts.
//  - for same length paths: earlier creation time goes first
type byPathLength []entry

func (l byPathLength) Len() int { return len(l) }

func (l byPathLength) Less(i, j int) bool {
	in, jn := len(l[i].Path), len(l[j].Path)
	if in == jn {
		return l[i].Creation.Before(l[j].Creation)
	}
	return in > jn
}

func (l byPathLength) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
